/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package com.cognition.util.j2me;

import com.cognition.util.marshal.IntegerMarshaller;
import com.cognition.util.marshal.StringMarshaller;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Hashtable;
import javax.microedition.rms.RecordEnumeration;
import javax.microedition.rms.RecordStore;
import javax.microedition.rms.RecordStoreException;
import javax.microedition.rms.RecordStoreFullException;
import javax.microedition.rms.RecordStoreNotFoundException;

/**
 * Creates a persisted hash table in the Record Management System.
 * 
 *
 * @author richard
 */
public class PersistedHashtable {

    private Hashtable hash;
    private String recordStoreName;
    private static Object obj = new Object();

    public PersistedHashtable(String name){
        if (name == null)
            throw new IllegalArgumentException("name cannot be null.");

        recordStoreName = name;
        restore();
    }

    /**
     * Loads the contents of the persistant storage into memory.  The in-memory
     * hash table is wiped out.
     * @return true if successful or false otherwise.
     */
    public boolean restore(){

        synchronized(obj){
            boolean result = true;

            try {
                Hashtable ht = new Hashtable();

                RecordStore store = RecordStore.openRecordStore(recordStoreName, true);
                for (RecordEnumeration en = store.enumerateRecords(null, null, true);
                    en.hasNextElement();){
                    byte[] b = en.nextRecord();
                    String key = extractKey(b);
                    byte[] value = extractValue(b);
                    ht.put(key, value);

                }
                store.closeRecordStore();
                hash = ht;
            } catch (RecordStoreFullException ex) {
                ex.printStackTrace();
            } catch (RecordStoreNotFoundException ex) {
                ex.printStackTrace();
            } catch (RecordStoreException ex) {
                ex.printStackTrace();
            }


            return result;
        }

    }
    /**
     * Save the hash table to memory. True is returned if the whole table
     * is saved. false if there's an error.
     */
    public boolean persist(){
        synchronized(obj){
            boolean result = true;

            try {
                RecordStore.deleteRecordStore(recordStoreName);

                RecordStore store = RecordStore.openRecordStore(recordStoreName, true);
                for (Enumeration en = hash.keys(); en.hasMoreElements(); ){

                    String key = (String) en.nextElement();
                    byte[] value = (byte[])hash.get(key);
                    if (!persist(key, value, store))
                        result = false;
                }
                store.closeRecordStore();
            } catch (RecordStoreFullException ex) {
                ex.printStackTrace();
            } catch (RecordStoreNotFoundException ex) {
                ex.printStackTrace();
            } catch (RecordStoreException ex) {
                ex.printStackTrace();
            }


            return result;
        }
    }

    /**
     * saves the key value pair to persistent storage
     * @param key the key to save
     * @param value the value to associate with the key.
     * @param store the record store to save the values in.
     * @return true if all records were saved or false if partial or no records were saved.
     */
    private boolean persist(String key, byte[] value, RecordStore store){

        if (key == null || key.length() <= 0)
            throw new IllegalArgumentException("parameters cannot be null");

        if (value == null)
            throw new IllegalArgumentException("parameters cannot be null");

        if (store == null)
            throw new IllegalArgumentException("parameters cannot be null");


        try{
            byte[] outBytes = StringMarshaller.marshal(key);

            // combine the key and value
            ByteArrayOutputStream os = new ByteArrayOutputStream(4);
            os.write(IntegerMarshaller.marshal(outBytes.length));
            os.write(outBytes);
            os.write(value);

            outBytes = os.toByteArray();

            int recNumber = getRecordNumber(key, store);

            if (recNumber >= 0)
                store.setRecord(recNumber, outBytes, 0, outBytes.length);
            else
                store.addRecord(outBytes, 0, outBytes.length);
                // update a current record number

            
        } catch (IOException ex){
            ex.printStackTrace();
            return false;
        } catch (RecordStoreException ex){
            ex.printStackTrace();
            return false;
        }

        return true;

    }

    /**
     * Assumes the key string is at the front of the array.  Extracts it.
     * @param b the byte array directly from storage (RMS).
     * @return the string that is the key.
     */
    private static String extractKey(byte[] b){
        if (b == null || b.length < 4)
            throw new IllegalArgumentException(
                    "byte array can't be null or too short");

        byte[] val = new byte[4];
        System.arraycopy(b, 0, val, 0, 4);
        int lenInt = IntegerMarshaller.unmarshal(val);

        if (lenInt <= 0)
            return "";

        val = new byte[lenInt];
        System.arraycopy(b, 4, val, 0, lenInt);

        return StringMarshaller.unmarshal(val);
        
    }


    /**
     * Assumes the key string is at the front of the array.  Extracts the value.
     * @param b the byte array directly from storage (RMS).
     * @return the string that is the key.
     */
    private static byte[] extractValue(byte[] b){
        if (b == null || b.length < 4)
            throw new IllegalArgumentException(
                    "byte array can't be null or too short");

        ByteArrayInputStream is = new ByteArrayInputStream(b);
        is.reset();

        try{
            byte[] val = new byte[4];
            is.read(val);

            int lenInt = IntegerMarshaller.unmarshal(val);
            if (lenInt <= 0)
                return new byte[0];
            is.skip(lenInt);
            if (is.available()<=0)
                return new byte[0];

            val = new byte[is.available()];
            is.read(val);

            return val;

        } catch (IOException ex){
            return new byte[0];
        }


        

    }
    public static int getRecordNumber(String k, RecordStore store){

        if (k == null || k.length() <= 0)
            return -1;
        
        try{

            for (   RecordEnumeration en = store.enumerateRecords(null, null, true);
                    en.hasNextElement();){

                int i = en.nextRecordId();
                byte[] b = store.getRecord(i);
                String storedKey = extractKey(b);

                if (storedKey != null && storedKey.equals(k))
                        return i;
                


            }
        } catch (RecordStoreException ex){
            ex.printStackTrace();
        }


        return -1;


    }

    public void clear(){
        synchronized(obj){
            hash.clear();
            try{
                RecordStore.deleteRecordStore(recordStoreName);
            } catch (RecordStoreException ex){
                // do nothing.
            }
        }
    }

    public void put(String k, byte[] v){
        if (k == null)
            throw new IllegalArgumentException("key must not be null.");

        if (v == null)
            throw new IllegalArgumentException("value must not be null.");

        hash.put(k, v);
    }

    public byte[] get(String key){
        return (byte[]) hash.get(key);
    }




}
